/*****************************************************************************************************************/
/**	INITIALIZATION																								**/
/*****************************************************************************************************************/

/* Constructor */
Animation = class( function( self, ... )

	/* Default values */
	self.Keyframes			= {
		Keyframe( 0, 0 ),
		Keyframe( 1, 1 )
	}
	self.Time				= 0
	self.Timer				= AdvTimer( 0.01, 100, 1 )
	self.Timer.LinkedObject	= self
	self.Reversed			= false
	self.LinkedObject		= nil
	
	/* Read parameters */
	local keyframesParam	= select( 1, ... )
	local durationParam		= select( 2, ... )
	local loopParam			= select( 3, ... )
	
	/* Assign keyframes */
	if IsTable( keyframesParam ) then
		self.Keyframes		= {}
		local i = 1
		local skip = false
		for _,v in pairs(keyframesParam) do
			if IsValidInstance( v, Keyframe ) then
			
				/* Force first keyframe to position 0 */
				if i == 0 and v.Position != 0 then
					DEBUG:Msg( "WARNING: first keyframe was not set to position 0. Position was forced to 0.", "Animation.ctor" )
					v.Position = 0
				end
				
				/* Force last keyframe to position 1 */
				if v.Position > 1 then
					if self.Keyframes[i - 1].Position != 1 then
						DEBUG:Msg( "WARNING: last keyframe was not set to position 1. Position was forced to 1.", "Animation.ctor" )
						v.Position = 1
					else
						/* Case in which we have keyframes above 1 after already setting one to 1 */
						DEBUG:Msg( "WARNING: found a keyframe with position above 1. Skipped.", "Animation.ctor" )
						skip = true
					end
				end
				
				/* Add valid keyframes */
				if not skip then
					self.Keyframes[i] = v
					i = i + 1
				end
				
				skip = false
			end
		end
	end
	
	/* Assign duration */
	if IsNumber( durationParam ) and durationParam > 0 then
		self.Timer.Duration	= durationParam
	end
	
	/* Assign loop count */
	if loopParam != nil then
		self.Timer.LoopCount	= loopParam
	end
	
	/* Hooks */
	self.Timer.Tick			= self.Tick
	self.Timer.Ended		= self.Ended
	self.OnTick				= nil
	self.OnEnded			= nil
	self.OnKeyReached		= nil
	self.OnFirstKeyReached	= nil
	self.OnLastKeyReached	= nil
end )

/*****************************************************************************************************************/
/**	PROPERTIES																									**/
/*****************************************************************************************************************/

/* Set the speed of the animation */
Animation.SetSpeedRatio = function( self, value )
	
	/* Check value */
	if not IsNumber( value ) then
		DEBUG:ErrMsg( "A number value is required for the speed ratio. Got a " .. tostring( type( value ) ) .. " instead.", "Animation.SetSpeedRatio" )
		return false
	end
	
	/* Speed ratio must be greater than 0 */
	if value <= 0 then
		DEBUG:ErrMsg( "The speed ratio must be greater than 0. Got " .. tostring( value ) .. ".", "Animation.SetSpeedRatio" )
		return false
	end
	
	/* Assign the speed ratio */
	self.Timer.SpeedRatio = value
	
	/* Everything ok */
	return true
end

/* Set the duration */
Animation.SetDuration = function( self, value )

	/* Need number */
	if not IsNumber( value ) then
		DEBUG:ErrMsg( "A number is required. Got " .. type( value ) .. ".", "Animation.SetDuration" )
		return false
	end
	
	/* Update the duration */
	local oldDuration	= self.Timer.Duration
	self.Timer.Duration	= value
	
	/* Update the time */
	self.Time			= self.Time / oldDuration * value
end

/* Check if the animation is running */
Animation.IsRunning = function( self )
	return self.Timer.IsRunning
end

/* Check if the animation is paused */
Animation.IsPaused = function( self )
	return self.Timer.IsPaused
end

/* Get the time elapsed at a given keyframe */
function GetKeyTime( keyframe )
	return keyframe.Position * self.Timer.Duration
end

/* Get the frames elapsed at a given keyframe */
function GetKeyTotalFrames( keyframe )
	return keyframe.Position * self.Timer:GetTotalFrames()
end

/* Get the current position */
Animation.GetCurrentPosition = function( self )
	return self.Time / self.Timer.Duration
end

/* Sets the current position */
Animation.SetCurrentPosition = function( self, value )
	
	/* Need a number between 0 and 1 */
	if not IsNumber( value ) or value < 0 or value > 1 then
		DEBUG:ErrMsg( "Position must be a number between 0 and 1. Got " .. tostring( value ), "Animation.SetCurrentPosition" )
		return false
	end
	
	/* Set the position */
	self.Time = value * self.Timer.Duration
end

/* Get current position relative to keyframes */
Animation.GetCurrentPositionRelative = function( self, key1, key2 )
	
	/* Get the general current position */
	local curPos	= self:GetCurrentPosition()
	
	/* Debug output */
	if not IsNumber( curPos ) or curPos < 0 or curPos > 1 then
		DEBUG:Msg( "WARNING: position is invalid: " .. tostring( curPos ), "Animation.GetCurrentPositionRelative" )
	end
	
	/* Ensure we talk about keyframes */
	if not IsValidInstance( key1, Keyframe ) or not IsValidInstance( key, Keyframe ) then
	
		/* Find the surrounding keyframes */
		key1		= self:GetPreviousKeyframe( curPos )
		key2		= self:GetNextKeyframe( curPos )
		
		/* If we don't have a previous key then there was an error */
		if not key1 then
			DEBUG:ErrMsg( "No keyframes could be retrieved to calculate the current value.", "Animation.GetCurrentValue" )
			return nil
		end
		
		/* If we have a previous key but no next key then we reached the end */
		if not key2 then
			return key1.Value
		end
	end
	
	/* Return the relative position */
	local above = ( curPos - key1.Position )
	local step = ( key2.Position - key1.Position )
	
	/* Easing support */
	return math.EaseInOut(
		above / step,
		key1.EaseOut,
		key2.EaseIn )
end

/* Get the current value */
Animation.GetCurrentValue = function( self )
	
	/* Variables */
	local value			= nil
	
	/* Get the current position */
	local curPos		= self:GetCurrentPosition()
	
	/* Find the surrounding keyframes */
	local prevKey		= self:GetPreviousKeyframe( curPos )
	local nextKey		= self:GetNextKeyframe( curPos )
	
	/* If we don't have a previous key then there was an error */
	if not prevKey then
		DEBUG:ErrMsg( "No keyframes could be retrieved to calculate the current value.", "Animation.GetCurrentValue" )
		return nil
	end
	
	/* If we have a previous key but no next key then we reached the end */
	if not nextKey then
		return prevKey.Value
	end
	
	/* Get the position relative to the keyframes (percent in between) */
	local relPos = self:GetCurrentPositionRelative( prevKey, nextKey )
	
	/* Interpolate between the keyframes */
	value			= MathHelper.Lerp( relPos, prevKey.Value, nextKey.Value )
	
	return value
end

/* Get the keyframe located before the given position */
Animation.GetPreviousKeyframe = function( self, position )

	/* If no position was specified, use the current position */
	if position == nil then
		position = self:GetCurrentPosition()

	/* Ensure valid position */
	elseif not IsNumber( position ) or position < 0 or position > 1 then
		DEBUG:ErrMsg( "Invalid position specified (" .. tostring( position ) .."), must be between 0 and 1.", "Animation.GetPreviousKeyframe" )
		return nil
	end
	
	/* Get & return the previous keyframe */
	for k,v in ipairs( self.Keyframes ) do
		if position >= v.Position then
		
			/* Found a higher keyframe, return the one before */
			if position < self.Keyframes[k + 1].Position then
				return v
				
			/* Found an equal keyframe, return that one */
			elseif position == self.Keyframes[k + 1].Position then
				return self.Keyframes[k + 1]
			end
		end
	end
	
	/* Not found (should never happen), return nil */
	return nil
end

/* Get the keyframe located after the given position */
Animation.GetNextKeyframe = function( self, position )

	/* If no position was specified, use the current position */
	if position == nil then
		position = self:GetCurrentPosition()

	/* Ensure valid position */
	elseif not IsNumber( position ) or position < 0 or position > 1 then
		DEBUG:ErrMsg( "Invalid position specified (" .. tostring( position ) .."), must be between 0 and 1.", "Animation.GetNextKeyframe" )
		return nil
	end
	
	/* Get & return the previous keyframe */
	for k,v in ipairs( self.Keyframes ) do
		if position < v.Position then
			return v
		end
	end
	
	/* Not found (only if last keyframe has been reached), return nil */
	return nil
end

/*****************************************************************************************************************/
/**	FUNCTIONS																									**/
/*****************************************************************************************************************/

/* Add a keyframe */
Animation.AddKeyframe = function( self, ... )
	
	/* Variables */
	local i					= 1
	local added				= false
	local keyframe			= nil
	
	/* Read parameter */
	local param1			= select( 1, ... )
	local param2			= select( 2, ... )
	local param3			= select( 3, ... )
	local param4			= select( 4, ... )
	
	/* Need a parameter */
	if !param1 then
		DEBUG:ErrMsg( "Requires one parameter of type 'Keyframe' or one of type integer and one of another type.", "Animation.AddKeyframe" )
		return false
	end
	
	/* Passed a Keyframe */
	if IsValidInstance( param1, Keyframe ) then
		keyframe = param1
		
	/* Passed keyframe position and potential value (and eventually ease in and ease out) */
	else
		keyframe = Keyframe( param1, param2, param3, param4 )
	end
		
	/* Position must be greater than 0 and less than 1 for a new keyframe */
	if param1.Position <= 0 or param1.Position >= 1 then
		DEBUG:ErrMsg(  "A newly added keyframe must be positioned between 0 and 1, both excluded.", "Animation.AddKeyframe" )
		return false
	end
	
	/* Add it to the list */
	for k,v in ipairs(self.Keyframes) do
	
		/* Found a greater position, start shifting from here */
		if param1.Position < v.Position then
			i = k
			local key1 = param1
			local key2 = self.Keyframes[i]
			while key1 do
				self.Keyframes[i] = key1
				key1 = key2
				key2 = self.Keyframes[i + 1]
				i = i + 1
			end
			added = true
			break
			
		/* Still smaller, just increase index */
		else
			i = i + 1
		end
	end
	
	/* If no greater one was found, insert at the end */
	if not added then
		self.Keyframes[i] = param1
	end
end

/* Play the animation */
Animation.Play = function( self )
	
	/* Start the timer */
	if self.Timer.Start then
		self.Timer:Start()
	end
end

/* Pause the animation */
Animation.Pause = function( self )
	
	/* Pause the timer */
	if self.Timer.Pause then
		self.Timer:Pause()
	end
end

/* Resume the animation */
Animation.Resume = function( self )

	/* Resume the timer */
	if self.Timer.Resume then
		self.Timer:Resume()
	end
end

/* Stop the animation */
Animation.Stop = function( self )
	
	/* Stop the timer */
	if self.Timer.Stop then
		self.Timer:Stop()
	end
end

/*****************************************************************************************************************/
/**	EVENTS																										**/
/*****************************************************************************************************************/

/* Animation tick */
Animation.Tick = function( advTimer )

	/* Do nothing if timer is not valid */
	if not advTimer:IsValid() then
		DEBUG:Msg( "Timer is not valid, skipping.", "Animation.Tick" )
		return
	end

	/* Retrieve the animation */
	local self		= advTimer.LinkedObject
	
	/* Update current time */
	if not self.Reversed then
		self.Time		= self.Time + advTimer:RealInterval()
	else
		self.Time		= self.Time - advTimer:RealInterval()
	end
	
	/* Always clamp position */
	self:SetCurrentPosition( math.Clamp( self:GetCurrentPosition(), 0, 1 ) )
	
	/* Get the previous & next keyframes */
	local prevKey = self:GetPreviousKeyframe()
	local nextKey = self:GetNextKeyframe()
	
	/* Debug output */
	local curPos		= self:GetCurrentPosition()
	local pkPos			= -1
	local nkPos			= -1
	local lpkPos		= -1
	local lnkPos		= -1
	if prevKey then pkPos = prevKey.Position end
	if nextKey then nkPos = nextKey.Position end
	if self.LastPrevKey then lpkPos = self.LastPrevKey.Position end
	if self.LastNextKey then lnkPos = self.LastNextKey.Position end
	
	/* Check if we reached the next keyframe */
	if not self.Reversed and prevKey == self.LastNextKey then
		
		/* Stick time to that key */
		self:SetCurrentPosition( prevKey.Position )
		
		/* Tick first (with clamped position) */
		if self.OnTick then
			self.OnTick( self )
		end
		
		/* Raise event if hooked */
		if self.OnKeyReached then
			self.OnKeyReached( self, prevKey )
		end
		
		/* Reached the end ? */
		if not nextKey then
			
			/* Raise event if hooked */
			if self.OnLastKeyReached then
				self:OnLastKeyReached( self, nextKey )
			end
		end
	
		/* Pause if the keyframe is stoppable */
		if prevKey:IsStoppable() then
			self:Pause()
		end
	
	/* Check if we reached the previous keyframe (remember next goes -1 at the end) */
	elseif self.Reversed and nextKey == self.LastPrevKey and lnkPos != -1 then
	
		/* Stick time to that key */
		self:SetCurrentPosition( nextKey.Position )
	
		/* Tick first (with clamped position) */
		if self.OnTick then
			self.OnTick( self )
		end
	
		/* Raise event if hooked */
		if self.OnKeyReached then
			self.OnKeyReached( self, nextKey )
		end
	
		/* Pause if the keyframe is stoppable */
		if nextKey:IsStoppable() then
			self:Pause()
		end
		
	/* Reaching first keyframe (0) */
	elseif self.Reversed and self:GetCurrentPosition() <= 0 then
	
		/* Stick time to that key */
		self:SetCurrentPosition( 0 )
	
		/* Tick first (with clamped position) */
		if self.OnTick then
			self.OnTick( self )
		end
	
		/* Raise event if hooked */
		if self.OnKeyReached then
			self.OnKeyReached( self, prevKey )
		end
		
		/* Raise event if hooked */
		if self.OnFirstKeyReached then
			self.OnFirstKeyReached( self, prevKey )
		end
	
		/* Pause if the keyframe is stoppable */
		if prevKey:IsStoppable() then
			self:Pause()
		end
		
	/* Reaching last keyframe (1) */
	elseif not self.Reversed and self:GetCurrentPosition() >= 1 then
	
		/* Stick time to that key */
		self:SetCurrentPosition( 1 )
	
		/* Tick first (with clamped position) */
		if self.OnTick then
			self.OnTick( self )
		end
	
		/* Raise event if hooked */
		if self.OnKeyReached then
			self.OnKeyReached( self, prevKey )
		end
		
		/* Raise event if hooked */
		if self.OnLastKeyReached then
			self.OnLastKeyReached( self, prevKey )
		end
	
		/* Pause if the keyframe is stoppable */
		if prevKey:IsStoppable() then
			self:Pause()
		end
	
	/* Otherwise, just tick */
	else
	
		/* Raise tick event if hooked */
		if self.OnTick then
			self.OnTick( self )
		end
	end
	
	/* Update last keyframe */
	self.LastPrevKey = prevKey
	self.LastNextKey = nextKey
end

/* Animation ended */
Animation.Ended = function( advTimer )

	/* Do nothing if timer is not valid */
	if not advTimer:IsValid() then
		DEBUG:Msg( "Timer is not valid, skipping.", "Animation.Ended" )
		return
	end

	/* Retrieve the animation */
	local self = advTimer.LinkedObject

	/* Raise event if hooked */
	if self.OnEnded then
		self.OnEnded( self )
	end
end